/**
 * Copyright 2014 opencxa.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.cxa.rpc.provider;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.cxa.misc.ClassUtils;
import org.cxa.misc.NumberUtils;
import org.cxa.rpc.messages.NotificationRpcMessage;
import org.cxa.rpc.messages.RequestRpcMessage;
import org.cxa.rpc.messages.ResponseRpcMessage;
import org.cxa.rpc.messages.RpcMessage;
import org.cxa.rpc.provider.RpcCommand.RestrictionType;
import org.cxa.rpc.provider.RpcException.RpcError;


/**
 * @author Christopher Armenio
 */
public class Component
{	
	public interface CatchAllHandler
	{
		public void onRequestReceived(RequestRpcMessage msgIn, String[] remainingPathComponentsIn);
		public void onResponseReceived(ResponseRpcMessage msgIn, String[] remainingPathComponentsIn);
		public void onNotificationReceived(NotificationRpcMessage msgIn);
	}
	
	
	private final String name;
	private Component parent = null;
	private final List<Component> subComponents = new ArrayList<Component>();
	private CatchAllHandler cah = null;
	
	
	public Component(String nameIn)
	{
		if( (nameIn == null) || nameIn.isEmpty() ) throw new IllegalArgumentException("name must be a non-null, non-empty String");
		if( nameIn.contains("/") ) throw new IllegalArgumentException("name must not contain '/'");
		this.name = nameIn;
	}
	
	
	public String getName()
	{
		return this.name;
	}
	
	
	public ComponentTreeProvider getRootTreeProvider()
	{
		return (this.parent != null) ? this.parent.getRootTreeProvider() : ((this instanceof ComponentTreeProvider) ? (ComponentTreeProvider)this : null);
	}
	
	
	public void addSubComponent(Component subComponentIn)
	{
		if( subComponentIn == null ) throw new IllegalArgumentException("subcomponent must be NOT be null");
		
		this.subComponents.add(subComponentIn);
		subComponentIn.parent = this;
	}
	
	
	public void setCatchAllHandler(CatchAllHandler cahIn)
	{
		this.cah = cahIn;
	}
	
	
	protected Component getSubComponent_byName(String nameIn)
	{
		for( Component currSubComp : this.subComponents )
		{
			if( currSubComp.getName().equals(nameIn) ) return currSubComp;
		}
		return null;
	}
	
	
	protected CatchAllHandler getCatchAllHandler()
	{
		return this.cah;
	}
	
	
	protected Object notify_onRequestReceived(RequestRpcMessage msgIn) throws RpcException
	{
		// search through all of our methods...
		for( Method currMethod : this.getClass().getMethods() )
		{
			if( (currMethod.getAnnotation(RpcCommand.class) != null) && currMethod.getName().equals(msgIn.getCommand()) )
			{
				// authenticate based upon annotation value
				RestrictionType restrictType = currMethod.getAnnotation(RpcCommand.class).value();
				if( restrictType == null ) throw RpcException.exceptionFromError(RpcError.INTERNAL_ERROR, "problem with restriction type, method is uncallable");
				switch( restrictType )
				{
					case PUBLIC:
						// nothing to do here!
						break;
						
					case USER_OR_GROUP:
						if( !msgIn.getPermissions_group() && !msgIn.getPermissions_user() ) throw RpcException.exceptionFromError(RpcError.INSUFFICIENT_PERMISSIONS);
						break;
						
					case USER_ONLY:
						if( !msgIn.getPermissions_user() ) throw RpcException.exceptionFromError(RpcError.INSUFFICIENT_PERMISSIONS);
						break;
				}
				
					
				// we got the right method...check our parameters and
				// try to coerce our parameters to the right type if needed...
				Object params[] = coerceParameters(currMethod, msgIn);

				// now call our method...
				try
				{
					return currMethod.invoke(this, params);
				}
				catch( InvocationTargetException e)
				{
					Throwable targetException = e.getTargetException();
					if( (targetException != null) && (targetException instanceof RpcException) ) throw (RpcException)targetException;
					else
					{
						throw RpcException.exceptionFromError(RpcError.ERROR_COMMAND_HANDLER, targetException.getMessage());
					}
				}
				catch( Exception e) { throw RpcException.exceptionFromError(RpcError.INTERNAL_ERROR, e.getMessage()); }
			}
		}
		
		// if we made it here, we couldn't find the proper method...
		throw RpcException.exceptionFromError(RpcError.UNKNOWN_COMMAND);
	}
	
	
	private static Object[] coerceParameters(Method methodIn, RequestRpcMessage msgIn)
	{
		Map<String, Object> coercedParameters = new HashMap<String, Object>();
		
		for( Parameter currParam : methodIn.getParameters() )
		{
			// special case...if the first parameter is an RpcMessage, the handler is
			// expecting the received message
			if( RpcMessage.class.isAssignableFrom(currParam.getType()) )
			{
				coercedParameters.put(currParam.getName(), msgIn);
				continue;
			}
			
			Object paramValue_raw = msgIn.getParameter_byName(currParam.getName());
			if( paramValue_raw == null ) throw RpcException.missingParameterTypeException(currParam.getName(), currParam.getType());
			
			// we have the right parameter name...now check the type
			if( currParam.getType().isAssignableFrom(paramValue_raw.getClass()) )
			{
				// simple case...we're good to go!
				coercedParameters.put(currParam.getName(), paramValue_raw);
			}
			else if( Number.class.isAssignableFrom(currParam.getType()) )
			{
				// we're expecting a number...check to see if we can strictly cast to it
				@SuppressWarnings("unchecked")
				Number paramCoerced = NumberUtils.castToClass_strict((Number)paramValue_raw, (Class<Number>)currParam.getType());
				if( paramCoerced == null ) throw RpcException.invalidParameterTypeException(currParam.getName(), currParam.getType());
				
				// if we made it here we were able to coerced to the proper number type...store in our translated parameters
				coercedParameters.put(currParam.getName(), paramCoerced);
			}
			else if( paramValue_raw instanceof Map )
			{
				// we have an incoming map, and our expected class is not a simple class...
				// so let's see if we can't pull a class from the json...
				Object paramCoerced = ClassUtils.classFromPojoMap(paramValue_raw, currParam.getType());
				if( paramCoerced == null ) throw RpcException.invalidParameterTypeException(currParam.getName(), currParam.getType());
				
				// if we made it here, we were able to translate to the proper class...stored in our coerced parameters
				coercedParameters.put(currParam.getName(), paramCoerced);
			}
			else throw RpcException.invalidParameterTypeException(currParam.getName(), currParam.getType());
		}
		
		// if we made it here, we found all of the parameters we need...now we need to assemble them...
		List<Object> retVal = new ArrayList<Object>();
		for( Parameter currParam : methodIn.getParameters() )
		{
			if( coercedParameters.containsKey(currParam.getName()) ) retVal.add(coercedParameters.get(currParam.getName()));
			else retVal.add(msgIn.getParameter_byName(currParam.getName()));
		}
		
		return retVal.toArray();
	}
}
